package com.martian.service.impl;

import com.github.pagehelper.PageHelper;
import com.martian.common.constant.TokenInfo;
import com.martian.common.enums.BaseErrorCodeEnum;
import com.martian.common.exception.SysException;
import com.martian.common.exception.UserException;
import com.martian.common.util.*;
import com.martian.mapper.MovieMapper;
import com.martian.mapper.UserMapper;
import com.martian.pojo.dto.*;
import com.martian.pojo.entity.*;
import com.martian.pojo.form.*;
import com.martian.service.UserService;
import com.martian.service.redis.RedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author martian
 * @version V1.0.0
 * @Description
 * @date 2023-03-24 9:53
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Resource
    UserMapper userMapper;
    @Resource
    MovieMapper movieMapper;

    @Resource
    private RedisRepository redisRepository;
    @Override
    public LoginSuccessDto userLogin(UserLoginForm userLoginForm) {
        UserLoginDto user = userMapper.selectByName(userLoginForm.getUserName());
        //密码经过md5加密处理
        String md5Password = Md5Util.MD5Encode(userLoginForm.getUserPassword()+user.getUserSalt());
        if (null == user || user.getUserIsAlive()==0) {
            log.info(userLoginForm.getUserName());
            log.info("用户名不存在或被冻结！");
            throw new UserException("用户名不存在或被冻结！");
        } else if (!user.getUserPassword().equals(md5Password)) {
            log.info("密码错误");
            throw new UserException("密码错误");
        } else {
//            判断是否异地登录，对应如下场景；在用户已经登录系统的情况下；用户换了一个浏览器或者在另外一台主机登录
            String oldToken = redisRepository.selectLoginAccessToken(user.getUserId());
            if (null != oldToken) {
                log.info("异地登录！");
                //如果存在异地登录，删除原有的token，刷新token
                redisRepository.deleteAccessToken(oldToken);
                redisRepository.deleteLoginAccessToken(user.getUserId());
            }
            //得到一个accessToken，生成tokenInfo
            TokenInfo tokenInfo = new TokenInfo();
            tokenInfo.setAccessToken(TokenUtil.genToken());
            tokenInfo.setUserId(user.getUserId());
            LoginSuccessDto messageSuccessLoginDto = new LoginSuccessDto(tokenInfo, user.getUserName(),user.getUserRatingNum());
            //保存这个用户和token信息
            redisRepository.saveAccessToken(tokenInfo);
            redisRepository.saveLoginAccessToken(tokenInfo);
            return messageSuccessLoginDto;
        }
    }

    public static String getUserWatchHistory(List<UserRatingHistory> userRatings){
        //1.构造候选集 阶段1的耗时在1~3s
        //1.1获取用户评分历史=观看历史
        List<UserRatingHistory> userRatingHistories = userRatings;
        if (userRatingHistories==null || userRatingHistories.size() ==0){
            return null;
        }
        //1.2从观看历史中找到用户最喜欢的电影类型
        //1.2.1统计用户评分电影的各类的数量
        Map<String,Integer> genreCount = new HashMap<>();
        for (UserRatingHistory userRatingHistory : userRatingHistories) {
            String[] genres = userRatingHistory.getMovieGenres().split("\\|");
            for (String genre : genres) {
                Integer count = genreCount.get(genre);
                if(count!=null){
                    genreCount.put(genre,count+1);
                }else{
                    genreCount.put(genre,1);
                }
            }
        }
        List<String> genres = new ArrayList<>();
        genreCount.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).forEach(m -> genres.add(m.getKey()));
        if (genres.size()<=5){
            return StringUtils.join(genres,"|");
        }else{
            return StringUtils.join(genres.subList(1,6),"|");
        }
    }

    /**
     * 召回层，根据用户的评分记录手动设置多路召回规则,根据用户喜欢的电影风格过滤，并减去自己已评分过的电影
     * @param userId
     * @param userlikeGenres
     * @return
     */
    public List<Integer> filterModel(Integer userId,String userlikeGenres,UserStatistic userStatistic){
        Integer targetYear = userStatistic.getUserAvgReleasedYear();
        //查询风格一致的并且在用户平均观影年份2年内的电影id
        //@TODO 按电影风格查询为正在表达式匹配，只要包含即可，这样得到的候选集数量较多，应该改为按照首个电影风格匹配
        List<Integer> candidateMovieIds = movieMapper.selectMovieIdsByGenresAndYear(userlikeGenres,targetYear,2);
        List<Integer> ratedMovieIds = movieMapper.selectUserRatingMovieIds(userId);
        //@TODO 前面的过滤法则是类型+与用户平均观看电影年份邻接两年内的，有可能比用户已经看过的电影要少，可能性非常小
        //将用户已经看过的电影去除
        if (ratedMovieIds.size()<candidateMovieIds.size()){
            candidateMovieIds = candidateMovieIds.stream().filter(e -> {
                return !ratedMovieIds.contains(e);
            }).collect(Collectors.toList());
        }
        return candidateMovieIds;
    }

    /**
     * 专门用于DeepCrossing和Wide&Deep排序层的召回层，返回的对象格式不同
     * @param userId
     * @param userlikeGenres
     * @param userStatistic
     * @return
     */
    public List<MovieStatistic> filterDeepModel(Integer userId,String userlikeGenres,UserStatistic userStatistic){
        Integer targetYear = userStatistic.getUserAvgReleasedYear();
        List<Integer> candidateMovieIds = movieMapper.selectMovieIdsByGenresAndYear(userlikeGenres,targetYear,2);
        List<Integer> ratedMovieIds = movieMapper.selectUserRatingMovieIds(userId);
        //前面的过滤法则是类型+与用户平均观看电影年份邻接两年内的，有可能比用户已经看过的电影要少，可能性很小
        if (ratedMovieIds.size()<candidateMovieIds.size()){
            candidateMovieIds = candidateMovieIds.stream().filter(e -> {
                return !ratedMovieIds.contains(e);
            }).collect(Collectors.toList());
        }
        return movieMapper.selectMovieStatisticsByIds(candidateMovieIds);
    }
    @Override
    public List<RecMovieDto> recMovieForYou(Integer size,Integer userId) {
        List<Integer> rankedList = recMovieIds(userId,size);
        if (rankedList.size() > size){
            List<Integer> rankedIds = rankedList.subList(0, size);
            List<RecMovieDto> result = movieMapper.selectRecMovieById(rankedIds);
            return result;
        }
        return movieMapper.selectRecMovieById(rankedList);
    }
    public List<Integer> recMovieIds(Integer userId,Integer size){
        //管理员也可调用该方法用于生产模型评价指标，管理员调用时传入参数，不使用localmap中的
        if (ThreadLocalMap.get("THREAD_LOCAL_KEY_LOGIN_MANAGER")==null){
            userId = (Integer) ThreadLocalMap.get("THREAD_LOCAL_KEY_LOGIN_USER");
        }
        if(userId <= 0 || size <=0){
            throw new SysException(BaseErrorCodeEnum.OVER_RANGE_ERROR);
        }
        Integer ab_test = userMapper.selectConfig();
        String model = "WideAndDeep";//默认排序算法
        if(ab_test==1){
            model = ABTest.getModelByUserId(userId.toString());
        }
        UserStatistic userStatistic = userMapper.selectUserStatistics(userId);
        //对于第一次登陆的用户，没有用户统计信息
        if (userStatistic == null){
            return movieMapper.getHotMovieIds(10);
        }
        String userlikeGenres = userStatistic.getUserLikeGenres();
        List<Integer> candidateMovieIds = null;
        List<MovieStatistic> candidateMovies = null;
        Map<Integer,Double> lastCandidateMovies = null;
        switch (model){
            case "emb":
                candidateMovieIds = filterModel(userId,userlikeGenres,userStatistic);
                lastCandidateMovies = embSimilar(userId,candidateMovieIds);//排序层
                break;
            case "NeuralCF":
                candidateMovieIds = filterModel(userId,userlikeGenres,userStatistic);
                lastCandidateMovies = neuralcf(userId,candidateMovieIds);
                break;
            case "DeepCrossing":
                candidateMovies = filterDeepModel(userId,userlikeGenres,userStatistic);
                lastCandidateMovies = deepCrossing(userId,candidateMovies);
                break;
            case "WideAndDeep":
                candidateMovies = filterDeepModel(userId,userlikeGenres,userStatistic);
                lastCandidateMovies = WideAndDeep(userId,candidateMovies);
                break;
        }

        List<Integer> rankedList = new ArrayList<>();
        lastCandidateMovies.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).forEach(m -> rankedList.add(m.getKey()));
        return rankedList;
    }

    @Override
    public void userlogout(UserLogOutForm userLogOutForm) {
        Integer userId = userLogOutForm.getUserId();
        Integer redisUserId = Integer.valueOf(redisRepository.selectAccessToken(userLogOutForm.getToken()));
        if (userId == redisUserId){
            redisRepository.deleteLoginAccessToken(userId);
            redisRepository.deleteAccessToken(userLogOutForm.getToken());
        }

    }

    @Override
    public int collectMovie(MovieCollect movieCollect) {
        try {
            int count = userMapper.selectMovieCollect(movieCollect);
            if (count==0){
                userMapper.insertMovieCollect(movieCollect);
                return 1;
            }else{
                userMapper.deleteMovieCollect(movieCollect);
                return 0;
            }
        }catch (Exception e){
            return -1;
        }

    }

    @Override
    public UserRatingHistoryDto getAllCollectedMovies(Integer page, Integer pageSize,Integer userId) {
        PageHelper.startPage(page,pageSize);
        List<UserRatingHistory> userRatingHistories = userMapper.selectAllMovieCollect(userId);
        Integer totalNum = movieMapper.getNumOfAllMovieCollect(userId);
        return new UserRatingHistoryDto(userRatingHistories,totalNum);
    }

    @Override
    public UserRatingHistoryDto getUserRatingHistory(Integer page, Integer pageSize, Integer userId) {
        if(userId <= 0 ){
            throw new SysException(BaseErrorCodeEnum.OVER_RANGE_ERROR);
        }
        PageHelper.startPage(page,pageSize);
        List<UserRatingHistory> userRatingHistories = userMapper.selectAllMovieRating(userId);
        Integer totalNum = movieMapper.getNumOfAllMovieRating(userId);
        return new UserRatingHistoryDto(userRatingHistories,totalNum);
    }
    @Override
    public List<HotUserDto> getHotUsers() {
        return userMapper.selectHotUsers();
    }

    @Override
    public AllUsersDto getAllUsers(Integer page, Integer pageSize) {
        if (page<=0 || pageSize>100){
            throw new SysException(BaseErrorCodeEnum.OVER_RANGE_ERROR);
        }
        PageHelper.startPage(page,pageSize);
        List<AllUsers> partUsers = userMapper.selectAllUsers();
        Integer totalNum = userMapper.getNumOfAllUsers();
        return new AllUsersDto(partUsers,totalNum);
    }

    @Override
    public void frozenUsers(FrozenUserForm frozenUserForm) {
        userMapper.frozenUsers(frozenUserForm.getUserIds(), Integer.valueOf(frozenUserForm.getFrozenTag()));
    }

    @Override
    public UserRatingHistoryDto getUsersScanHistory(Integer page, Integer pageSize) {
        Integer userId = (Integer) ThreadLocalMap.get("THREAD_LOCAL_KEY_LOGIN_USER");
        PageHelper.startPage(page,pageSize);
        UserScanHistoryDto userScanHistoryDto = redisRepository.selectUserScanHistory(userId);
        List<UserRatingHistory> userScanMovies = movieMapper.selectMoviesForUserHistory(userScanHistoryDto);
        Integer totalNum = Math.toIntExact(redisRepository.getNumOfUserHistory(userId));
        return new UserRatingHistoryDto(userScanMovies,totalNum);
    }

    private Map<Integer,Double> WideAndDeep(Integer userId, List<MovieStatistic> candidateMovieIds){
        DeepCrossingInputForm deepCrossingInputForm = userMapper.selectWideAndDeepInput(userId);
        JSONArray instances = new JSONArray();
        for (MovieStatistic candidateMovie : candidateMovieIds) {
            if (candidateMovie.getMovieRatingCount()==null){
                candidateMovie.setMovieRatingCount(1);//不想跳过，选择探索性更强的方法
            }
            deepCrossingInputForm.setMovieFeatures(candidateMovie);
            JSONObject instance = new JSONObject(deepCrossingInputForm);
            instances.put(instance);
        }
        JSONArray scores = ModelServing.predict(instances,"WideAndDeep");
        Map<Integer,Double> lastCandidateMovies = new HashMap<>();
        for (int i = 0 ; i < candidateMovieIds.size(); i++){
            lastCandidateMovies.put(candidateMovieIds.get(i).getMovieId(), scores.getJSONArray(i).getDouble(0));
        }
        return lastCandidateMovies;
    }
    private Map<Integer,Double> deepCrossing(Integer userId, List<MovieStatistic> candidateMovieIds){
        DeepCrossingInputForm deepCrossingInputForm = userMapper.selectDeepCrossingInput(userId);
        JSONArray instances = new JSONArray();
        for (MovieStatistic candidateMovie : candidateMovieIds) {
            if (candidateMovie.getMovieRatingCount()==null){
                candidateMovie.setMovieRatingCount(1);//不想跳过，选择探索性更强的方法
            }
            deepCrossingInputForm.setMovieFeatures(candidateMovie);
            JSONObject instance = new JSONObject(deepCrossingInputForm);
            instances.put(instance);
        }
        JSONArray scores = ModelServing.predict(instances,"DeepCrossing");
        Map<Integer,Double> lastCandidateMovies = new HashMap<>();
        for (int i = 0 ; i < candidateMovieIds.size(); i++){
            lastCandidateMovies.put(candidateMovieIds.get(i).getMovieId(), scores.getJSONArray(i).getDouble(0));
        }
        return lastCandidateMovies;
    }
    private Map<Integer,Double> neuralcf(Integer userId, List<Integer> candidateMovieIds){
        NeuralCfInputForm neuralCfInputForm = userMapper.selectNeuralCFInput(userId);
        String userGenre1 = neuralCfInputForm.getUserGenre1().split("\\|")[0];
        neuralCfInputForm.setUserGenre1(userGenre1);
        JSONArray instances = new JSONArray();
        for (Integer candidateMovieId : candidateMovieIds) {
            if(userGenre1.equals("")){
                continue;
            }
            neuralCfInputForm.setMovieId(candidateMovieId);
            JSONObject instance = new JSONObject(neuralCfInputForm);
            instances.put(instance);
        }
        JSONArray scores = ModelServing.predict(instances,"NeuralCF");
        Map<Integer,Double> lastCandidateMovies = new HashMap<>();
        for (int i = 0 ; i < candidateMovieIds.size(); i++){
            lastCandidateMovies.put(candidateMovieIds.get(i), scores.getJSONArray(i).getDouble(0));
        }
        return lastCandidateMovies;
    }
    private Map<Integer,Double> embSimilar(Integer userId,List<Integer> candidateMovieIds){
        //2.1查询redis获取用户Emb
        String userStringEmb = redisRepository.selectUserEmb(userId);
        //2.2 将用户Emb转换为List<Float>
        Embedding user = new Embedding();
        user.setEmbedVector(Embedding.parseStrToEmbArray(userStringEmb));
        //2.3遍历候选电影的Emb并计算相似度 耗时最长，在20s左右
        Map<Integer,Double> lastCandidateMovies = new HashMap<>();
        List<Movie> movies = new ArrayList<>();
        for (Integer candidateMovieId : candidateMovieIds) {
            String movieStrEmb = redisRepository.selectMovieItemEmb(candidateMovieId);
        //因为最初数据集中，电影emb采用Item2Vec，采样时将平均评分小于3.5电影去掉了
            if (movieStrEmb==null){
                continue;
            }
            Embedding movieEmb = new Embedding(Embedding.parseStrToEmbArray(movieStrEmb));
            Movie movie = new Movie();
            movie.setMovieId(candidateMovieId);
            movie.setEmb(movieEmb);
            movies.add(movie);
        }
        //排序层采用embedding计算相似度排序
        for (Movie movie : movies) {
            Double similarity = user.calculateSimilarity(movie.getEmb());
            lastCandidateMovies.put(movie.getMovieId(),similarity);
        }
        return lastCandidateMovies;
    }
}
